더블 버퍼링 깜빡임 없는 부드러운 화면 -완-

더블 버퍼링 깜빡임 없는 화면 그리기

더블 버퍼링 화면에 바로 그리지 않고, 메모리 공간에 먼저 그린 뒤 한 번에 출력하는 구조를 사용하면 깜빡임을 거의 완전히 제거할 수 있습니다.
화면에 도형을 그리기 시작하면 바로 눈에 들어오는 문제가 있습니다. 바로 화면이 번쩍이며 깜빡이는 현상입니다. 특히 마우스로 계속 그리거나 자주 갱신할수록 더 두드러지게 나타납니다.

왜 화면이 깜빡이는가

깜빡임의 원인은 단순합니다. 화면을 지우고 다시 그리기 때문입니다.

기본 흐름은 다음과 같습니다.

  • 배경 지우기 (WM_ERASEBKGND)
  • WM_PAINT에서 다시 그림

이 과정이 반복되면서 사용자에게는 깜빡임으로 보이게 됩니다.

흔한 오해: 코드를 줄이면 해결될까

코드를 줄이거나 그리기 횟수를 줄이는 방법은 근본적인 해결이 되지 않습니다.

  • 일부 상황에서만 완화됨
  • 구조가 그대로라면 깜빡임 유지

문제는 “얼마나 많이 그리느냐”가 아니라 “어떻게 그리느냐”입니다.

해결 방법: 더블 버퍼링 구조 이해하기

더블 버퍼링은 보이지 않는 메모리 공간에 먼저 그림을 완성한 뒤, 결과만 화면에 한 번에 출력하는 방식입니다.

핵심 흐름은 다음과 같습니다.

  1. 메모리 DC 생성
  2. 메모리 DC에 전체 화면 그리기
  3. BitBlt로 한 번에 출력

이 방식은 중간 과정이 화면에 보이지 않기 때문에 깜빡임이 사라집니다.

실제 구현: 더블 버퍼링 적용하기

case WM_PAINT:
{
    PAINTSTRUCT ps;
    HDC hdc = BeginPaint(hwnd, &ps);

    RECT rc;
    GetClientRect(hwnd, &rc);

    HDC memDC = CreateCompatibleDC(hdc);
    HBITMAP memBitmap = CreateCompatibleBitmap(hdc, rc.right, rc.bottom);
    HBITMAP oldBitmap = (HBITMAP)SelectObject(memDC, memBitmap);

    // 배경 채우기
    FillRect(memDC, &rc, (HBRUSH)(COLOR_WINDOW+1));

    // 실제 그리기
    Ellipse(memDC, g_x - 20, g_y - 20, g_x + 20, g_y + 20);

    // 화면에 출력
    BitBlt(hdc, 0, 0, rc.right, rc.bottom, memDC, 0, 0, SRCCOPY);

    // 리소스 복구
    SelectObject(memDC, oldBitmap);
    DeleteObject(memBitmap);
    DeleteDC(memDC);

    EndPaint(hwnd, &ps);
}
return 0;

추가로 깜빡임을 완전히 줄이려면 다음도 함께 적용하는 것이 좋습니다.

  • WM_ERASEBKGND에서 return 1 처리
  • 배경을 한 번만 그리도록 통제

이 두 가지를 적용하면 이중으로 지워지는 문제를 방지할 수 있습니다.

마무리

이 단계까지 오면 WinAPI의 핵심 흐름을 모두 경험한 상태입니다.

  • 창 생성
  • 메시지 처리
  • 입력 처리
  • GDI 그리기
  • UI 컨트롤
  • 구조 개선
  • 렌더링 최적화

이제 단순 예제가 아니라, 실제 프로그램을 만들 수 있는 기반이 완성되었습니다

웹 크롤러를 직접 만들어보니 구조가 보였다

웹 크롤러

어느 날 평소처럼 사이트를 관리하다가 문득 궁금해졌습니다. 검색엔진은 내가 보는 사이트를 과연 똑같이 보고 있을까요?

방문자 입장에서는 메뉴도 잘 보이고 글도 문제없이 읽힙니다. 하지만 직접 웹 크롤러를 만들어 사이트를 수집해본 후 생각이 완전히 달라졌습니다.

SEO를 공부하면서 내부링크, 사이트맵, 크롤링 예산 같은 용어를 자주 접했지만 실제로 체감하기는 어려웠습니다. 그런데 내 사이트를 크롤러 시선으로 바라보니 왜 이런 개념들이 중요한지 한 번에 이해할 수 있었습니다.

크롤러를 만들게 된 단순한 이유

처음부터 SEO 분석을 목적으로 크롤러를 만든 것은 아니었습니다.

사이트 규모가 커지면서 내가 작성한 글이 몇 개인지, 어떤 페이지들이 서로 연결되어 있는지 확인하고 싶었습니다. 그래서 파이썬으로 간단한 크롤러를 만들었습니다.

시작 URL을 입력하면 내부 링크를 따라 이동하면서 URL과 제목을 저장하는 구조였습니다.

처음에는 특별한 문제가 발견되지 않을 것이라고 생각했습니다. 어차피 내가 만든 사이트였고 구조도 잘 알고 있다고 믿었기 때문입니다.

그런데 결과를 확인하는 순간 예상과 다른 상황이 나타났습니다. 운영자가 알고 있는 사이트와 검색엔진이 보는 사이트는 생각보다 큰 차이가 있었습니다.

웹 크롤러 방식

내 사이트를 크롤링하자마자 발견된 문제들

좋은 콘텐츠를 만드는 것과 검색엔진이 콘텐츠를 발견하는 것은 전혀 다른 문제였습니다.

링크가 끊긴 페이지

몇몇 페이지는 주소를 변경하거나 삭제한 적이 있었는데 오래된 링크가 그대로 남아 있었습니다.

사용자는 거의 발견하지 못하지만 크롤러는 모든 링크를 따라갑니다. 그 결과 여러 개의 404 오류 페이지가 확인됐습니다.

이런 오류가 누적되면 크롤링 효율이 떨어지고 사이트 품질 관리 측면에서도 좋지 않은 영향을 줄 수 있습니다.

고립된 페이지(Orphan Page)

가장 의외였던 부분은 오펀 페이지였습니다.

분명 내가 작성한 글인데 어떤 페이지에서도 연결되어 있지 않았습니다. URL을 직접 입력해야만 접근 가능한 상태였습니다.

사이트 운영자는 해당 글의 존재를 알고 있지만 크롤러는 그렇지 않습니다. 링크가 없다면 해당 페이지는 발견 자체가 어려워질 수 있습니다.

그 순간 내부링크가 단순한 사용자 편의 기능이 아니라 검색엔진의 길이라는 사실을 실감했습니다.

중복 URL

같은 내용을 담고 있는데 URL만 다른 페이지도 발견됐습니다.

카테고리 경로 차이, 파라미터 URL, 슬래시 유무 같은 작은 차이 때문에 동일한 콘텐츠가 여러 주소로 노출되고 있었습니다.

사이트 규모가 커질수록 이런 문제는 검색엔진이 페이지를 이해하는 데 혼란을 줄 수 있습니다.

검색엔진은 생각보다 링크에 의존한다

크롤러를 만들면서 가장 크게 깨달은 사실은 검색엔진이 링크를 통해 사이트를 이해한다는 점이었습니다.

내부링크가 크롤링 경로가 되는 이유

크롤러는 기본적으로 링크를 따라 이동합니다.

즉 중요한 페이지라면 내부링크를 통해 충분히 연결되어 있어야 합니다.

실제로 여러 페이지에서 연결된 콘텐츠는 빠르게 발견됐고, 반대로 연결이 부족한 페이지는 수집 우선순위가 낮았습니다.

내가 중요하다고 생각하는 페이지와 검색엔진이 중요하게 인식하는 페이지가 반드시 같은 것은 아니라는 점도 알게 됐습니다.

관련글과 카테고리 구조의 중요성

예전에는 관련 글 기능을 체류시간 증가용 기능 정도로 생각했습니다.

하지만 크롤러 입장에서는 새로운 콘텐츠를 발견하는 주요 통로였습니다.

카테고리 구조 역시 마찬가지였습니다.

잘 정리된 카테고리는 검색엔진이 사이트 전체 구조를 이해하는 데 큰 도움을 주고 있었습니다. 반대로 연결이 부족한 콘텐츠는 존재 자체를 알리기 어려웠습니다.

크롤링 데이터로 확인한 SEO 약점들

직접 데이터를 살펴보니 평소에는 보이지 않던 약점들이 드러나기 시작했습니다.

페이지 깊이(Depth)

중요한 글인데도 메인 페이지에서 다섯 번 이상 클릭해야 접근 가능한 경우가 있었습니다.

사용자뿐 아니라 크롤러도 이런 페이지를 상대적으로 늦게 발견할 수 있습니다.

중요한 콘텐츠일수록 가능한 한 얕은 구조에 배치하는 것이 유리하다는 점을 데이터로 확인할 수 있었습니다.

상태코드 문제

404 오류 외에도 여러 단계의 리다이렉트가 발견됐습니다.

사용자는 최종 페이지에 도착하므로 큰 문제를 느끼지 못합니다.

하지만 크롤러는 모든 이동 과정을 처리해야 하기 때문에 비효율이 발생할 수 있습니다.

사이트가 커질수록 이런 문제는 점점 누적됩니다.

크롤러가 이해하기 어려운 페이지 특징

일부 페이지는 콘텐츠가 존재함에도 불구하고 접근성이 좋지 않았습니다.

자바스크립트 의존도가 높거나 링크 구조가 지나치게 복잡한 경우였습니다.

최근 검색엔진은 발전했지만 여전히 단순하고 명확한 구조를 선호합니다.

크롤러를 직접 만들어보면 이런 문제를 생각보다 자주 발견하게 됩니다.

웹 크롤러 문제

사이트맵보다 더 중요했던 것은 구조였다

사이트맵만 제출하면 SEO 문제가 대부분 해결될 것이라고 생각했던 시기가 있었습니다.

실제 크롤링 결과는 조금 달랐습니다.

XML 사이트맵의 역할

사이트맵은 검색엔진에게 페이지 목록을 알려주는 역할을 합니다.

특히 신규 페이지나 내부링크가 부족한 페이지를 발견하는 데 도움을 줄 수 있습니다.

실제로 크롤러에 사이트맵 분석 기능을 추가했을 때 내부 링크만으로는 발견하지 못했던 페이지들이 나타났습니다.

링크 구조와 사이트맵의 차이

사이트맵은 주소 목록입니다.

반면 내부링크는 실제 이동 경로입니다.

검색엔진은 두 가지 정보를 함께 활용해 사이트를 이해합니다.

크롤러가 실제로 발견한 결과

건강한 페이지들은 공통점이 있었습니다.

사이트맵에 포함되어 있었고 내부링크도 충분했습니다.

반대로 문제가 있는 페이지는 둘 중 하나가 부족한 경우가 많았습니다.

결국 사이트맵보다 중요한 것은 사이트 전체 구조였습니다.

이 경험은 SEO를 바라보는 시각 자체를 바꿔놓았습니다. 예전에는 좋은 글을 많이 작성하면 검색엔진이 자연스럽게 평가해줄 것이라고 생각했습니다. 하지만 크롤러를 직접 만들어보니 아무리 좋은 콘텐츠라도 검색엔진이 발견하지 못하면 기회조차 얻기 어렵다는 사실을 알게 됐습니다.

기술 SEO 역시 복잡한 최적화 작업이 아니라 검색엔진이 사이트를 이해하기 쉽게 만드는 과정이라는 점을 체감했습니다. 내부링크, 상태코드, URL 구조 같은 요소들이 중요하게 여겨지는 이유도 결국 같은 맥락이었습니다.

무엇보다 SEO는 검색엔진을 속이기 위한 기술이 아니라 검색엔진이 콘텐츠를 쉽게 발견하고 이해할 수 있도록 돕는 작업에 가깝다는 생각을 하게 됐습니다. 흥미로운 점은 검색엔진이 이해하기 쉬운 사이트가 사용자에게도 편리한 경우가 대부분이었다는 점입니다.

웹 크롤러 구조

SEO는 결국 검색엔진에게 길을 알려주는 작업이었다

직접 크롤링을 해보기 전까지 SEO는 다소 추상적인 개념이었습니다.

하지만 크롤러 시선으로 사이트를 바라본 후에는 왜 내부링크와 구조가 중요한지 훨씬 명확하게 이해할 수 있었습니다.

실제로 사이트를 점검하면서 404 오류는 없는지, 검색엔진이 도달할 수 없는 오펀 페이지는 없는지, 중요한 글이 지나치게 깊은 위치에 숨어 있지는 않은지 자연스럽게 확인하게 됐습니다. 또한 내부링크가 충분히 연결되어 있는지, 사이트맵이 최신 상태로 유지되고 있는지도 이전보다 자주 살펴보게 됐습니다.

이런 점검 항목들은 구글 SEO 가이드에서도 반복적으로 강조하는 내용인데, 직접 크롤러를 만들어보니 왜 그런 권장사항이 존재하는지 몸소 이해할 수 있었습니다.

 

참고 자료

– Moz Blog
https://moz.com/blog

– 랭크온
https://akeep.co.kr

구조 개선 하기: 코드 정리와 리팩토링

구조 개선 : 유지보수 가능한 코드로 리팩토링하기

구조 개선 과정은 선택이 아니라 필수입니다. 메시지 기반 구조 특성상 모든 로직을 WndProc에 넣으면 코드가 빠르게 복잡해지고 유지보수가 어려워집니다. 핵심은 이벤트 분기, 상태 관리, 기능 분리를 명확히 나누는 것입니다.
버튼과 입력창까지 구현했다면 이제 코드가 길어지고 수정이 점점 어려워졌을 것입니다. 이 단계에서는 기능을 확장하기 전에 구조를 정리해야 이후 작업이 훨씬 수월해집니다.

왜 구조 개선 이 필수 단계가 되는가

처음에는 WndProc 하나에 모든 코드를 넣어도 문제가 없어 보입니다. 하지만 기능이 늘어나면 다음 문제가 바로 나타납니다.

  • 메시지 처리 코드가 계속 증가
  • 전역 변수 관리가 어려워짐
  • UI와 로직이 뒤섞임

WinAPI는 메시지 기반 구조이기 때문에 한 곳에 로직이 몰리면 수정 시 영향 범위를 파악하기 어려워집니다.

1 – WndProc은 이벤트 분기만 담당하기

WndProc은 “무슨 일이 발생했는지 판단하는 역할”까지만 담당해야 합니다. 실제 동작은 별도의 함수로 분리하는 것이 핵심입니다.

case WM_LBUTTONDOWN:
    OnClick(hwnd, lParam);
    return 0;
void OnClick(HWND hwnd, LPARAM lParam)
{
    int x = LOWORD(lParam);
    int y = HIWORD(lParam);

    // 실제 처리 로직
}

이렇게 하면 WndProc은 이벤트 라우터 역할만 하게 되어 코드가 훨씬 읽기 쉬워집니다.

2 – 전역 변수 대신 상태 구조체 사용하기

전역 변수는 초기에는 편하지만 규모가 커질수록 문제를 일으킵니다. 상태를 하나의 구조로 묶는 것이 훨씬 안전합니다.

struct AppState
{
    int x;
    int y;
    std::wstring text;
};
방식 특징
전역 변수 간단하지만 확장성 낮음
구조체 상태 관리 명확, 유지보수 용이

상태를 구조체로 묶으면 코드 흐름을 이해하기 쉬워지고, 기능 추가 시 충돌을 줄일 수 있습니다.

3 – 기능별 함수 분리하기

WinAPI 프로그램은 다음 세 역할로 나눌 수 있습니다.

  • 입력 처리
  • 화면 그리기
  • UI 이벤트 처리
void Render(AppState& state, HDC hdc)
{
    TextOut(hdc, 100, 100, state.text.c_str(), state.text.length());
}
void HandleInput(AppState& state, LPARAM lParam)
{
    state.x = LOWORD(lParam);
    state.y = HIWORD(lParam);
}

상태를 참조로 전달해야 변경 내용이 유지됩니다. 값으로 전달하면 수정이 반영되지 않는 문제가 발생합니다.

4 – 클래스 기반 구조로 확장하기

규모가 커지면 구조체만으로는 한계가 생깁니다. 이때는 클래스 기반으로 확장하는 것이 좋습니다.

class App
{
public:
    void OnClick(LPARAM lParam);
    void Render(HDC hdc);

private:
    int x;
    int y;
    std::wstring text;
};
구조 특징
구조체 상태 관리 중심
클래스 상태 + 기능 통합

이 방식은 상태와 로직을 함께 관리하기 때문에 프로젝트 규모가 커질수록 효과가 커집니다.
구조를 개선하지 않은 상태에서 기능을 계속 추가하면, 코드 수정 하나가 전체 동작에 영향을 주는 상황이 발생합니다. 반대로 이 구조를 적용하면 기능을 독립적으로 확장할 수 있습니다.
이 단계까지 오면 WinAPI를 단순 학습이 아니라 실제 프로젝트 수준으로 확장할 수 있는 기반을 갖추게 됩니다.

버튼 추가하기: 컨트롤의 시작

버튼 입력창으로 프로그램 형태 갖추기

버튼 입력창을 추가하면, 단순한 그래픽 프로그램에서 실제로 사용자와 상호작용하는 애플리케이션으로 확장됩니다. 핵심은 컨트롤을 생성하고, WM_COMMAND 메시지로 이벤트를 처리하는 구조입니다.
이전 단계에서 GDI로 도형을 그렸다면, 이제는 사용자가 직접 입력하고 결과를 확인하는 흐름까지 이어지게 됩니다.

GDI 다음에 컨트롤을 배우는 이유

WinAPI에서 화면을 구성하는 방식은 두 가지로 나뉩니다.

구분 특징
GDI 직접 그리기, 자유도 높음
컨트롤 미리 만들어진 UI, 사용 간편

그래픽 출력 중심에서 사용자 인터페이스 구성 중심으로 넘어가는 단계입니다. 컨트롤을 활용하면 빠르게 프로그램 형태를 갖출 수 있습니다.

버튼 추가하기: BUTTON 클래스로 클릭 가능한 요소 만들기

버튼은 “BUTTON” 클래스를 사용하여 생성합니다.

#define ID_BUTTON 1

HWND hButton = CreateWindow(
    L"BUTTON",
    L"확인",
    WS_VISIBLE | WS_CHILD,
    50, 50, 100, 30,
    hwnd,
    (HMENU)ID_BUTTON,
    hInstance,
    NULL
);

컨트롤 ID는 매크로로 정의해두는 것이 좋습니다. 버튼에서 발생한 이벤트는 부모 윈도우로 전달되기 때문에, WndProc에서 처리해야 합니다.

입력창 추가하기: EDIT 클래스로 텍스트 입력 받기

입력창은 “EDIT” 클래스를 사용합니다.

#define ID_EDIT 2

HWND hEdit = CreateWindow(
    L"EDIT",
    L"",
    WS_VISIBLE | WS_CHILD | WS_BORDER,
    50, 100, 200, 25,
    hwnd,
    (HMENU)ID_EDIT,
    hInstance,
    NULL
);

입력창은 반드시 핸들을 저장해두어야 합니다. 이후 입력값을 읽을 때 이 핸들을 사용합니다.

버튼 클릭으로 입력값 읽기

컨트롤 이벤트는 모두 WM_COMMAND 메시지를 통해 부모 윈도우로 전달됩니다.

case WM_COMMAND:
{
    if (LOWORD(wParam) == ID_BUTTON && HIWORD(wParam) == BN_CLICKED)
    {
        wchar_t buffer[100];
        GetWindowText(hEdit, buffer, 100);

        MessageBox(hwnd, buffer, L"입력값", MB_OK);
    }
}
return 0;

핵심 구조는 다음과 같습니다.

  • LOWORD(wParam): 어떤 컨트롤인지 구분
  • HIWORD(wParam): 어떤 이벤트인지 구분

이 흐름을 통해 버튼 클릭 → 입력값 읽기 → 결과 출력까지 연결됩니다.
입력값이 비어 있는 경우는 대부분 입력창 핸들이 올바르게 전달되지 않았거나, 잘못된 변수를 사용한 경우입니다.
이 단계까지 오면 WinAPI에서 기본적인 사용자 인터페이스를 갖춘 프로그램 구조를 완성하게 됩니다.

GDI 기초: 도형과 텍스트 그리기

GDI 기초 / 그리기 확장: 선, 사각형, 원 그려보기

GDI 기초 과정을 통해 직접 그리는 구조를 이해해야 합니다. 입력으로 좌표를 받고, 그 좌표를 기준으로 WM_PAINT에서 다시 그리는 방식이 핵심입니다.
이전 단계에서 입력 처리까지 구현했다면, 이제 단순 텍스트를 넘어 도형을 직접 그려보는 단계로 넘어가게 됩니다. 이 과정에서 “출력”이 아니라 “렌더링”이라는 개념이 자연스럽게 연결됩니다.

Hello World와 입력 처리 다음에 GDI 를 배우는 이유

WinAPI의 흐름은 다음처럼 확장됩니다.

  • Hello World → 화면 출력
  • 입력 처리 → 사용자 반응
  • GDI → 화면 구성

이 단계부터는 프로그램이 단순히 결과를 보여주는 것이 아니라, 화면 자체를 직접 만들어가는 구조로 바뀝니다.

STEP 1 – GDI 와 HDC 개념 이해하기

GDI의 핵심은 HDC입니다. 이는 화면에 그리기 위한 디바이스 컨텍스트 핸들로, 모든 그리기 작업의 기준이 됩니다.

PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);

// 그리기 작업 수행

EndPaint(hwnd, &ps);

BeginPaint와 EndPaint 사이에서만 안정적으로 그리기가 이루어집니다. 이 구조를 벗어나면 화면이 정상적으로 갱신되지 않을 수 있습니다.

STEP 2 – 선 그리기: MoveToEx와 LineTo 사용하기

선은 가장 기본적인 도형이며, 시작점과 끝점을 이용해 그립니다.

  1. 시작 위치 설정 (MoveToEx)
  2. 끝 위치까지 선 그리기 (LineTo)
MoveToEx(hdc, 50, 50, NULL);
LineTo(hdc, 200, 200);

MoveToEx로 시작 위치를 지정하지 않으면 예상과 다른 위치에서 선이 그려질 수 있습니다.

STEP 3 – 사각형과 원 그리기

사각형과 원은 경계 좌표를 이용해 간단하게 그릴 수 있습니다.

Rectangle(hdc, 50, 50, 200, 150);
Ellipse(hdc, 220, 50, 350, 150);

이 함수들은 왼쪽 위와 오른쪽 아래 좌표를 기준으로 도형을 생성합니다. 기본 색상이 적용되는 이유는 GDI의 기본 펜과 브러시가 사용되기 때문입니다.

STEP 4 – 입력 처리와 그리기 연결하기

GDI를 실제 프로그램처럼 사용하려면 입력 처리와 반드시 연결해야 합니다.

기본 흐름은 다음과 같습니다.

  • 입력 발생
  • 좌표 저장
  • InvalidateRect 호출
  • WM_PAINT에서 도형 그리기
case WM_LBUTTONDOWN:
{
    g_x = LOWORD(lParam);
    g_y = HIWORD(lParam);

    InvalidateRect(hwnd, NULL, TRUE);
}
return 0;
case WM_PAINT:
{
    PAINTSTRUCT ps;
    HDC hdc = BeginPaint(hwnd, &ps);

    Ellipse(hdc, g_x - 20, g_y - 20, g_x + 20, g_y + 20);

    EndPaint(hwnd, &ps);
}
return 0;

이 구조를 통해 클릭 위치에 원을 그릴 수 있습니다.
여기서 중요한 점은 WM_PAINT가 화면을 매번 새로 그린다는 것입니다. 그래서 이전 도형이 사라지는 현상이 발생할 수 있습니다. 여러 개의 도형을 유지하려면 좌표를 하나가 아니라 리스트 형태로 저장해야 합니다.
이 개념까지 이해하면 WinAPI에서 단순 출력이 아닌, 실제 그래픽 프로그램의 기초를 갖추게 됩니다.

입력 처리하기: 마우스와 키보드 이벤트

입력 처리 클릭과 키보드로 창 반응시키기

입력 처리는 “메시지를 받아 상태를 바꾸고, 다시 그린다”는 흐름으로 동작합니다. 이 구조를 이해하면 클릭이나 키 입력에 따라 화면이 변하는 프로그램을 만들 수 있습니다.
Hello World까지 출력했다면 이런 생각이 들 수 있습니다. “이 창은 왜 아무 반응이 없을까?” 실제 프로그램이라면 클릭하거나 키를 눌렀을 때 화면이 바뀌어야 자연스럽게 느껴집니다. 이번 단계에서는 WinAPI 창이 사용자 입력에 반응하고, 그 결과를 화면에 반영하는 흐름까지 이어서 만들어보겠습니다.

Hello World 다음에는 왜 입력 처리를 배워야 할까?

WinAPI 프로그램은 입력을 처리해야 비로소 “동작하는 프로그램”처럼 보입니다.
이전 단계에서는 WM_PAINT 메시지를 통해 텍스트를 화면에 출력했습니다. 하지만 이 상태는 단순히 보여주기만 하는 정적인 화면입니다. 사용자의 행동에 따라 변화가 있어야 실제 프로그램처럼 느껴집니다.

핵심 흐름은 다음과 같습니다.

  • 입력 → 상태 변경 → 화면 다시 그리기

이 세 단계가 연결되면서 프로그램이 실제로 반응하기 시작합니다.

마우스 클릭 처리하기: WM_LBUTTONDOWN 이해하기

마우스를 클릭하면 WM_LBUTTONDOWN 메시지가 발생합니다. 이 메시지를 통해 클릭 위치와 이벤트를 처리할 수 있습니다.

case WM_LBUTTONDOWN:
{
    int x = LOWORD(lParam);
    int y = HIWORD(lParam);

    g_text = L"마우스를 클릭했습니다";
    InvalidateRect(hwnd, NULL, TRUE);
}
return 0;

lParam에는 클릭 좌표가 들어 있습니다. LOWORD와 HIWORD를 이용하면 X, Y 값을 분리할 수 있습니다.
중요한 점은 단순히 메시지를 처리하는 것이 아니라, 상태값을 바꾸고 화면 갱신까지 연결해야 한다는 것입니다.

키보드 입력 처리하기: WM_KEYDOWN 이해하기

키보드 입력은 WM_KEYDOWN 메시지로 처리합니다. 단, 창이 포커스를 가지고 있어야 메시지가 들어옵니다.

case WM_KEYDOWN:
{
    if (wParam == 'A')
    {
        g_text = L"A 키를 눌렀습니다";
        InvalidateRect(hwnd, NULL, TRUE);
    }
}
return 0;

wParam에는 눌린 키 코드가 들어 있습니다. 이를 통해 특정 키 입력에 따라 동작을 다르게 만들 수 있습니다.
여기서 자주 발생하는 실수는 창에 포커스가 없는 상태에서 키 입력을 테스트하는 것입니다. 반드시 창을 한 번 클릭한 후 입력을 확인해야 합니다.

입력 결과를 화면에 반영하는 흐름 만들기

입력 처리의 핵심은 “값 변경과 화면 갱신은 별개”라는 점입니다.

다음 흐름을 반드시 기억해야 합니다.

  • 입력 메시지 수신
  • 상태값 변경 (예: g_text)
  • InvalidateRect 호출
  • WM_PAINT에서 다시 그림
case WM_PAINT:
{
    PAINTSTRUCT ps;
    HDC hdc = BeginPaint(hwnd, &ps);

    TextOut(hdc, 100, 100, g_text.c_str(), g_text.length());

    EndPaint(hwnd, &ps);
}
return 0;

이 구조를 통해 클릭이나 키 입력에 따라 화면 텍스트가 바뀌는 프로그램을 만들 수 있습니다.
많은 초보자가 “값은 바뀌었는데 화면이 그대로인” 문제를 겪습니다. 대부분 InvalidateRect 호출이 빠진 경우입니다.
이 흐름을 이해하면 WinAPI에서 입력과 화면 출력이 어떻게 연결되는지 명확해집니다. 이제 단순 출력 프로그램을 넘어, 사용자와 상호작용하는 기본 형태를 갖춘 상태입니다.

WinAPI 입문기 하나하나 배워보자

WinAPI 첫 걸음: Hello World 윈도우 만들기

WinAPI 프로그램에서 윈도우를 만들려면 “클래스 등록 → 윈도우 생성 → 메시지 처리”라는 흐름을 반드시 따라야 합니다. 이 구조만 이해하면 처음 보는 코드도 훨씬 쉽게 읽히게 됩니다.
처음 Visual Studio에서 WinAPI 프로젝트를 실행해 보면, 창이 보이지 않거나 잠깐 나타났다 바로 종료되는 경우를 자주 겪게 됩니다. 콘솔 프로그램과 달리 구조가 눈에 보이지 않기 때문입니다. 이 글에서는 실제로 “Hello World”가 화면에 출력되는 윈도우를 단계별로 만들어 보겠습니다.

WinAPI 프로그램의 기본 구조 이해하기

WinAPI 프로그램은 main이 아닌 WinMain에서 시작됩니다. 그리고 일반적인 함수 호출 흐름이 아니라, “메시지 기반 구조”로 동작합니다.

핵심 흐름은 다음 세 단계로 이어집니다.

  • 윈도우 클래스 등록 (설계도 정의)
  • 윈도우 생성 (실제 창 생성)
  • 메시지 처리 (동작 정의)

이 구조를 이해하면 코드가 왜 복잡해 보이는지도 자연스럽게 납득할 수 있습니다. 특히 클래스를 등록하지 않으면 윈도우를 생성할 수 없습니다.

STEP 1 – 윈도우 클래스 등록하기

윈도우를 만들기 위해서는 먼저 WNDCLASS 구조체를 통해 “창의 성격”을 정의해야 합니다.

핵심 요소는 다음과 같습니다.

  1. lpfnWndProc: 메시지를 처리할 함수
  2. hInstance: 프로그램 인스턴스
  3. lpszClassName: 클래스 이름
WNDCLASS wc = {0};
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.lpszClassName = L"HelloWindow";

RegisterClass(&wc);

여기서 L 접두어는 UNICODE 문자열을 의미합니다. WinAPI는 기본적으로 유니코드를 사용하기 때문에 반드시 붙여주는 것이 안전합니다.
자주 발생하는 실수는 RegisterClass 실패를 확인하지 않는 것입니다. 이 경우 이후 단계도 모두 실패하게 됩니다.

STEP 2 – 실제 윈도우 생성 및 표시

클래스를 등록했다면 이제 실제 창을 생성합니다.

HWND hwnd = CreateWindow(
    L"HelloWindow",
    L"Hello World Window",
    WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT, CW_USEDEFAULT,
    500, 300,
    NULL, NULL, hInstance, NULL
);

ShowWindow(hwnd, nCmdShow);

CreateWindow는 창의 크기, 위치, 스타일을 결정합니다. 그리고 ShowWindow를 호출해야 화면에 표시됩니다.
이 단계에서 가장 흔한 실수는 ShowWindow를 빠뜨리는 것입니다. 실행은 되지만 창이 보이지 않는 원인이 됩니다.

STEP 3 – 메시지 처리와 Hello World 출력

이제 창은 만들어졌지만, 아직 내용은 비어 있습니다. 화면에 무엇을 그릴지는 직접 정의해야 합니다.

WndProc 함수에서 WM_PAINT 메시지를 처리하면 됩니다.

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_PAINT:
    {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hwnd, &ps);

        TextOut(hdc, 100, 100, L"Hello World", 11);

        EndPaint(hwnd, &ps);
    }
    return 0;

    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }

    return DefWindowProc(hwnd, msg, wParam, lParam);
}

WM_PAINT는 화면을 다시 그릴 때 호출되는 메시지입니다. 이 안에서 TextOut을 사용하면 문자열을 출력할 수 있습니다.

마지막으로 메시지 루프가 필요합니다.

  • 메시지를 받아야 창이 유지됩니다
  • 루프가 없으면 프로그램이 바로 종료됩니다
MSG msg = {};
while (GetMessage(&msg, NULL, 0, 0))
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

이 구조까지 완성하면 WinAPI의 기본 흐름이 모두 연결됩니다.
윈도우 생성 → 메시지 처리 → 화면 출력까지 이어지며, 정상적인 GUI 프로그램이 동작하게 됩니다.